/*
 * ConverterScanner.java January 2010
 *
 * Copyright (C) 2010, Niall Gallagher <niallg@users.sf.net>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package wx.xml.simpleframework.xml.convert;

import java.lang.annotation.Annotation;

import wx.xml.simpleframework.xml.Element;
import wx.xml.simpleframework.xml.Root;
import wx.xml.simpleframework.xml.strategy.Type;
import wx.xml.simpleframework.xml.strategy.Value;

/**
 * The <code>ConverterScanner</code> is used to create a converter
 * given a method or field representation. Creation of the converter
 * is done using the <code>Convert</code> annotation, which may
 * be used to annotate a field, method or class. This describes the
 * implementation to use for object serialization. To account for
 * polymorphism the type scanned for annotations can be overridden
 * from type provided in the <code>Type</code> object. This ensures
 * that if a collection of objects are serialized the correct
 * implementation will be used for each type or subtype.
 *
 * @author Niall Gallagher
 */
class ConverterScanner {

    /**
     * This is used to instantiate converters given the type.
     */
    private final ConverterFactory factory;

    /**
     * This is used to build a scanner to scan for annotations.
     */
    private final ScannerBuilder builder;

    /**
     * Constructor for the <code>ConverterScanner</code> object. This
     * uses an internal factory to instantiate and cache all of the
     * converters created. This will ensure that there is reduced
     * overhead for a serialization process using converters.
     */
    public ConverterScanner() {
        this.factory = new ConverterFactory();
        this.builder = new ScannerBuilder();
    }

    /**
     * This method will lookup and instantiate a converter found from
     * scanning the field or method type provided. If the type has
     * been overridden then the <code>Value</code> object will provide
     * the type to scan. If no annotation is found on the class, field
     * or method then this will return null.
     *
     * @param type  this is the type to search for the annotation
     * @param value this contains the type if it was overridden
     * @return a converter scanned from the provided field or method
     */
    public Converter getConverter(Type type, Value value) throws Exception {
        Class   real    = getType(type, value);
        Convert convert = getConvert(type, real);

        if (convert != null) {
            return factory.getInstance(convert);
        }
        return null;
    }

    /**
     * This method will lookup and instantiate a converter found from
     * scanning the field or method type provided. If the type has
     * been overridden then the object instance will provide the type
     * to scan. If no annotation is found on the class, field or
     * method then this will return null.
     *
     * @param type  this is the type to search for the annotation
     * @param value this contains the type if it was overridden
     * @return a converter scanned from the provided field or method
     */
    public Converter getConverter(Type type, Object value) throws Exception {
        Class   real    = getType(type, value);
        Convert convert = getConvert(type, real);

        if (convert != null) {
            return factory.getInstance(convert);
        }
        return null;
    }

    /**
     * This method is used to scan the provided <code>Type</code> for
     * an annotation. If the <code>Type</code> represents a field or
     * method then the annotation can be taken directly from that
     * field or method. If however the type represents a class then
     * the class itself must contain the annotation.
     *
     * @param type the field or method containing the annotation
     * @param real the type that represents the field or method
     * @return this returns the annotation on the field or method
     */
    private Convert getConvert(Type type, Class real) throws Exception {
        Convert convert = getConvert(type);

        if (convert == null) {
            return getConvert(real);
        }
        return convert;
    }

    /**
     * This method is used to scan the provided <code>Type</code> for
     * an annotation. If the <code>Type</code> represents a field or
     * method then the annotation can be taken directly from that
     * field or method. If however the type represents a class then
     * the class itself must contain the annotation.
     *
     * @param type the field or method containing the annotation
     * @return this returns the annotation on the field or method
     */
    private Convert getConvert(Type type) throws Exception {
        Convert convert = type.getAnnotation(Convert.class);

        if (convert != null) {
            Element element = type.getAnnotation(Element.class);

            if (element == null) {
                throw new ConvertException("Element annotation required for %s", type);
            }
        }
        return convert;
    }

    /**
     * This method is used to scan the provided <code>Type</code> for
     * an annotation. If the <code>Type</code> represents a field or
     * method then the annotation can be taken directly from that
     * field or method. If however the type represents a class then
     * the class itself must contain the annotation.
     *
     * @param real the type that represents the field or method
     * @return this returns the annotation on the field or method
     */
    private Convert getConvert(Class real) throws Exception {
        Convert convert = getAnnotation(real, Convert.class);

        if (convert != null) {
            Root root = getAnnotation(real, Root.class);

            if (root == null) {
                throw new ConvertException("Root annotation required for %s", real);
            }
        }
        return convert;
    }

    /**
     * This is used to acquire the <code>Convert</code> annotation from
     * the class provided. If the type does not contain the annotation
     * then this scans all supertypes until either an annotation is
     * found or there are no further supertypes.
     *
     * @param type  this is the type to scan for annotations
     * @param label this is the annotation type that is to be found
     * @return this returns the annotation if found otherwise null
     */
    private <T extends Annotation> T getAnnotation(Class<?> type, Class<T> label) {
        return builder.build(type).scan(label);
    }

    /**
     * This is used to acquire the class that should be scanned. The
     * type is found either on the method or field, or should there
     * be a subtype then the class is taken from the provided value.
     *
     * @param type  this is the type representing the field or method
     * @param value this contains the type if it was overridden
     * @return this returns the class that has been scanned
     */
    private Class getType(Type type, Value value) {
        Class real = type.getType();

        if (value != null) {
            return value.getType();
        }
        return real;
    }

    /**
     * This is used to acquire the class that should be scanned. The
     * type is found either on the method or field, or should there
     * be a subtype then the class is taken from the provided value.
     *
     * @param type  this is the type representing the field or method
     * @param value this contains the type if it was overridden
     * @return this returns the class that has been scanned
     */
    private Class getType(Type type, Object value) {
        Class real = type.getType();

        if (value != null) {
            return value.getClass();
        }
        return real;
    }
}
